/*
 * Copyright 2011 Angel Sanadinov
 *
 * This file is part of VBox WMI.
 *
 * VBox WMI is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * VBox WMI is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with VBox WMI.  If not, see <http://www.gnu.org/licenses/>.
 */

package Utilities;

import Beans.PermissionsBean;
import Beans.ServerBean;
import Managers.DatabaseManager;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.io.UnsupportedEncodingException;
import java.sql.Timestamp;
import java.util.Random;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.ws.WebServiceException;

/**
 * This class contains various static functions.
 *
 * @author Angel Sanadinov
 */
public class Functions
{
    /** Regular expression for matching a valid media and machines location (file name) */
    public static final Pattern locationPattern = Pattern.compile("[^/\"\\\\|`?*<>:\n\t\f\r]+");
    /** Regular expression for retrieving "interesting" VirtualBox exception data */
    public static final Pattern virtualBoxExceptionPattern =
            Pattern.compile("VirtualBox\\serror:\\s(.*)\\s\\(0x(.*)\\)");
    
    private Functions(){}

    /**
     * Hashes the supplied plaintext password and returns the hash.
     *
     * @param plainTextPassword the password to be hashed
     * @param hashingAlgorithm the algorithm to be used when hashing the password
     * @param charsetName the charset to be used by the hashing function
     * @return the hashed password
     * @throws NoSuchAlgorithmException if the hashing algorithm is not implemented by any provider
     * @throws UnsupportedEncodingException if the charset that is used is not supported
     */
    public static String hashPassword(final String plainTextPassword, String hashingAlgorithm, String charsetName)
            throws NoSuchAlgorithmException, UnsupportedEncodingException
    {
        String result = null;

        //retrieves the digest algorithm
        MessageDigest digest = MessageDigest.getInstance(hashingAlgorithm);
        //hashes the password and retrieves the result
        result = new String(digest.digest(plainTextPassword.getBytes(charsetName)), charsetName);

        return result;
    }

    /**
     * Checks if the supplied user id is valid.
     *
     * @param userId the user id to be checked
     * @return <code>true</code> if the id is valid or <code>false</code> otherwise
     */
    public static boolean isUserIdValid(final int userId)
    {
        if(userId > Constants.INVALID_USER_ID)
            return true;
        else
            return false;
    }

    /**
     * Checks if the supplied server id is valid.
     *
     * @param serverId the server id to be checked
     * @return <code>true</code> if the id is valid or <code>false</code> otherwise
     */
    public static boolean isServerIdValid(final int serverId)
    {
        if(serverId > Constants.INVALID_SERVER_ID)
            return true;
        else
            return false;
    }

    /**
     * Checks if the supplied permissions are valid.
     *
     * @param permissions the permissions to be checked
     * @return <code>true</code> if the permissions are valid or <code>false</code> otherwise
     */
    public static boolean arePermissionsValid(final String permissions)
    {
        //checks if the String is not null and if it is of the proper length
        if(permissions != null && permissions.length() == 3)
        {
            try
            {
                //attempts to parse the String to an integer, which would throw
                //an exception if it is not an integer (permissions are not valid)
                Integer.parseInt(permissions);
                return true;
            }
            catch(NumberFormatException e)
            {
                return false;
            }
        }
        else
            return false;
    }

    /**
     * Checks if the supplied timestamp is valid.
     *
     * @param timestamp the timestamp to be checked
     * @return <code>true</code> if the timestamp is valid or <code>false</code> otherwise
     */
    public static boolean isTimestampValid(final Timestamp timestamp)
    {
        if(timestamp != null)
            return true;
        else
            return false;
    }

    /**
     * Checks if the supplied UUID is valid. <br><br>
     *
     * <b>Note:</b> <i>UUIDs are used for virtual resources and are created by VirtualBox.</i>
     *
     * @param uuid the ID to be checked
     * @return <code>true</code> if the UUID is valid or <code>false</code> otherwise
     */
    public static boolean isUUIDValid(final String uuid)
    {
        if(uuid != null)
        {
            try
            {
                //if no exception is thrown, the UUID is valid
                UUID.fromString(uuid);
                return true;
            }
            catch(IllegalArgumentException e)
            {}
        }
        else
            ;

        return false;
    }

    /**
     * Checks if the specified location is valid. <br><br>
     *
     * These locations are used to define where an IMedium image or an IMachine is on the host.
     *
     * @param location the location to be checked
     * @return <code>true</code> if the location is valid or <code>false</code> otherwise
     */
    public static boolean isLocationValid(final String location)
    {
        return locationPattern.matcher(location).matches();
    }

    /**
     * Checks that the controller port is within valid range. <br><br>
     *
     * The unity of the sets of valid controller ports for each controller type
     * gives a range from 0 to 29 (inclusive).
     *
     * @param port the port number to be checked
     * @return <code>true</code> if the controller port number is within range
     *         or <code>false</code> otherwise
     */
    public static boolean isControllerPortValid(final int port)
    {
        if(port >= 0 && port <= 29)
            return true;
        else
            return false;
    }

    /**
     * Checks that the controller slot is valid. <br><br>
     *
     * 0 is a valid controller slot, with 1 also valid for some controller types.
     *
     * @param slot the slot number to be checked
     * @return <code>true</code> if the slot number is valid or <code>false</code> otherwise
     */
    public static boolean isControllerSlotValid(final int slot)
    {
        if(slot == 0 || slot == 1)
            return true;
        else
            return false;
    }

    /**
     * Checks if the specified user is considered to be server manager. <br><br>
     *
     * The manager status is verified by checking the permissions of the user on
     * a specific server. If the user has full access for machines, media and networks
     * that user is considered to have manager status.
     *
     * @param userId the id of the user to be checked
     * @param permissions the permissions of that user on a server
     * @return <code>true</code> if the user is a server manager or <code>false</code>
     *         otherwise
     */
    public static boolean isUserServerManager(final int userId, final PermissionsBean permissions)
    {
        String managerPermissions = "777";

        if(userId == permissions.getUserId()
                && permissions.getMachinesPermissions().equals(managerPermissions)
                && permissions.getMediaPermissions().equals(managerPermissions)
                && permissions.getNetworksPermissions().equals(managerPermissions))
            return true;
        else
            return false;
    }

    /**
     * Allocates an available range of ports on the specified server. <br><br>
     * 
     * The database is modified accordingly and no further action is needed, except
     * to set the returned VRDP ports in the machine's VRDP server. <br><br>
     *
     * <b>Note:</b> <i>This function should be called on machine creation only.</i>
     *
     * @param manager the database manager to be used for the operation
     * @param serverId the ID of the server on which the machine resides
     * @return the allocated VRDP ports which can be directly set in the machine's
     *         VRDP server configuration
     * @throws IllegalArgumentException if some or all of the input is invalid
     */
    public static String allocateVRDPPorts(DatabaseManager manager, int serverId)
            throws IllegalArgumentException
    {
        //checks if a database manager was supplied and if the server ID is valid
        if(manager != null && isServerIdValid(serverId))
        {
            //retrieves the server data
            ServerBean serverData = manager.getServer(serverId);

            //checks the server data
            if(serverData != null && serverData.isValid())
            {
                //creates a new random number generator
                Random generator = new Random();

                //calculates the number of port slots for the current server
                int numberOfSlots = (serverData.getVrdpPortsRangeHigh() - serverData.getVrdpPortsRangeLow())
                                    / serverData.getVrdpPortsPerMachine();

                //retrieves all slots currently allocated
                String[] slots = serverData.getVrdpAllocatedSlots().split(";");

                //checks if there are free slots
                if(slots.length >= numberOfSlots)
                    return null;
                else
                    ;

                boolean done = false;
                int generatedSlot = -1;
                Pattern newPattern = null;
                StringBuilder expression = new StringBuilder();

                //attemtps to generate a new free slot
                do
                {
                    //generates a slot
                    generatedSlot = generator.nextInt(numberOfSlots);

                    //builds a regular expression which is to be used to check if the slot is free
                    expression.delete(0, expression.length());
                    expression.append(";");
                    expression.append(generatedSlot);
                    expression.append(";|;");
                    expression.append(generatedSlot);
                    expression.append("$");
                    newPattern = Pattern.compile(expression.toString());

                    //checks if the pattern found anything for the current slot
                    if(!newPattern.matcher(serverData.getVrdpAllocatedSlots()).find())
                        done = true; //nothing was found, the slot is free
                    else
                        ;
                } while(!done);

                //checks if a valid slot was generated
                if(generatedSlot >= 0)
                {
                    //updates the database with the newly allocated slot
                    manager.updateServerVRDPAllocationSlots(serverId,
                            serverData.getVrdpAllocatedSlots() + ";" + generatedSlot);

                    //generates the first VRDP port, based on the new slot and the server data
                    int firstPort = serverData.getVrdpPortsRangeLow() 
                                    + generatedSlot*serverData.getVrdpPortsPerMachine();

                    //builds the VRDP ports string
                    String result = "";
                    for(int i = 0; i < serverData.getVrdpPortsPerMachine(); i++)
                        result += (firstPort + i) + ",";

                    //returns the ports string (w/o the trailing comma)
                    return result.substring(0, result.length() - 1);
                }
                else
                    return null;
            }
            else
                throw new IllegalArgumentException("Invalid server ID, server does not exist.");
        }
        else
            throw new IllegalArgumentException("No database manager or invalid server ID supplied.");
    }

    /**
     * Frees the supplied VRDP ports from the specified server. <br><br>
     * The database is modified accordingly and no further action is needed. <br><br>
     *
     * <b>Note:</b> <i>This function should be called on machine removal only.</i>
     *
     * @param manager the database manager to be used for the operation
     * @param serverId the ID of the server on which the machine resides
     * @param ports the current VRDP ports of the machine
     *
     * @throws IllegalArgumentException if some or all of the input is invalid
     */
    public static void freeVRDPPorts(DatabaseManager manager, int serverId, String ports)
            throws IllegalArgumentException
    {
        //checks if the ports are non-empty
        if(ports == null || ports.trim().equals(""))
            return; //nothing for removal
        else
            ;

        //checks if a database manager was supplied and if the server ID is valid
        if(manager != null && isServerIdValid(serverId))
        {
            //retrieves the server data
            ServerBean serverData = manager.getServer(serverId);

            //checks the server data
            if(serverData != null && serverData.isValid())
            {
                //retrieves the first VRDP port and based on the server data
                //determines the allocated slot
                int slot = (Integer.parseInt(ports.split(",")[0]) - serverData.getVrdpPortsRangeLow())
                           / serverData.getVrdpPortsPerMachine();

                //builds the regular expression that will be used to match the slot
                StringBuilder expression = new StringBuilder();
                expression.append(";");
                expression.append(slot);
                expression.append("(;)|;");
                expression.append(slot);
                expression.append("$");

                //removes the current slot from the list of allocated slots
                manager.updateServerVRDPAllocationSlots(serverId,
                        serverData.getVrdpAllocatedSlots().replaceFirst(expression.toString(), "$1"));
            }
            else
                throw new IllegalArgumentException("Invalid server ID, server does not exist.");
        }
        else
            throw new IllegalArgumentException("No database manager or invalid server ID supplied.");
    }

    /**
     * Parses the supplied VirtualBox exception. <br><br>
     *
     * The exception's code and message are retrieved, and are used to build an
     * <code>ApplicationException</code> object.
     *
     * @param exception the VirtualBox exception
     * @return the generated application exception object
     */
    public static ApplicationException parseVirtualBoxException(final WebServiceException exception)
    {
        ApplicationException result = null;

        //creates the exception matcher
        Matcher exceptionMatcher = virtualBoxExceptionPattern.matcher(exception.getMessage());
        //checks if the supplied exception messages matches the patter for regular VirtualBox exceptions
        if(exceptionMatcher.find())
            result = new ApplicationException(exceptionMatcher.group(2), exceptionMatcher.group(1));
        else
            result = new ApplicationException("-1", "Failed to parse exception message: " + exception.getMessage());

        return result;
    }
}
